
/*
 * @Author: c
 * @Date: 2024-01-09 12:24:59
 * @LastEditTime: 2024-04-02 16:27:01
 * @LastEditors: c
 * @Description:
 * @FilePath: \music-web-backend\src\core\music\song\song.service.ts
 * https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
 */
import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { plainToClass } from 'class-transformer';
import { EXCEPTION } from 'src/common/const/constants';
import { IOption } from 'src/common/dto/option.dto';
import { PageResDto } from 'src/common/dto/page.dto';
import { AudioRateEnum } from 'src/common/enums/audio.enum';
import { CommonException } from 'src/common/exception/common.exception';
import { RateFileInfo } from 'src/core/music/rate_file_info/entity/audio.enity';
import { FileInfo } from 'src/core/system/minio/entities/file_info.entity';
import { FileFormatEnum } from 'src/core/system/minio/enum';
import { MinioClientService } from 'src/core/system/minio/minio.service';
import { User } from 'src/core/system/user/entities/user.entity';
import { DataSource, Repository } from 'typeorm';
import { AlbumService } from '../album/album.service';
import { ArtistService } from '../artist/artist.service';
import { Artist } from '../artist/entities/artist.entity';
import { Category } from '../category/entities/category.entity';
import { VideoForm } from '../video/dto/create-video.dto';
import { Video } from '../video/entities/video.entity';
import { DSong } from './dao/song.dao';
import { FavoriteForm, MusicUploadFile, SongForm } from './dto/create-song.dto';
import { SongDto, SongPageQuery, TrackLinkQueryDto, TrackLinkVO } from './dto/search-song.dto';
import { Song } from './entities/song.entity';

@Injectable()
export class SongService {



  constructor(
    @Inject(forwardRef(() => ArtistService))
    private readonly artistService: ArtistService,
    @Inject(forwardRef(() => AlbumService))
    private readonly albumService: AlbumService,
    @InjectRepository(Song) private songRepository: Repository<Song>,
    private dataSource: DataSource,
    private minioClientService: MinioClientService,
    private dsong: DSong,

  ) { }




  /**
* 歌曲-歌手所有歌曲
* @param pageDto 
* @returns 
*/
  async getArtistSongList(user: User, pageDto: SongPageQuery) {
    const [list, total] = await this.dsong.getArtistSongList(user?.username, pageDto);
    return PageResDto.result(list, total)
  }

  /**
   * 歌曲-获取单个播放链接
   * @param param0 
   */
  async getTrackLink(req: any, { id }: TrackLinkQueryDto): Promise<TrackLinkVO> {
    let userRole = "user"
    const song = await this.findOne(req, id)
    const data: TrackLinkVO = new TrackLinkVO()
    let audioInfo
    if (song.isVip || song.isPaid) {
      audioInfo = song.rateFileInfos.filter(rateFile => rateFile.isFull && rateFile.rate === AudioRateEnum.UNDAMAGED).at(0)
    } else if (song.isVip && !song.isPaid && userRole !== 'vip') {
      //提供试听版本
      // audioInfo = song.rateFileInfos.filter(rateFile => !rateFile.isFull).at(0)
      audioInfo = song.rateFileInfos.filter(rateFile => rateFile.isFull && rateFile.rate === AudioRateEnum.UNDAMAGED).at(0)
    } else {
      //不是VIP用户，提供低质音频
      // audioInfo = song.rateFileInfos.filter(rateFile => rateFile.isFull && rateFile.rate === AudioRateEnum.LOSSY).at(0)
      audioInfo = song.rateFileInfos.filter(rateFile => rateFile.isFull && rateFile.rate === AudioRateEnum.UNDAMAGED).at(0)
    }

    data.duration = audioInfo.duration
    data.rate = audioInfo.rate
    data.startTime = audioInfo.startTime
    data.expireTime = audioInfo.expiredTime + Date.now()
    data.path = (await this.minioClientService.presignedGetObject(audioInfo.fileInfo.bucket, audioInfo.fileInfo.objectName, audioInfo.expiredTime)).url


    return data
  }


  /**
   * 歌曲-修改
   * @param songId 
   * @param songForm 
   * @returns 
   */
  async update(songId: number, songForm: SongForm) {

    let data = plainToClass(Song, songForm)
    const artists = songForm.artistIds.map(id => new Artist({ id }))

    //修改类别
    const cate = new Category(+songForm.genre)


    //修改封面
    let newPics = []
    for await (const p of songForm.pic) {
      const pictureFile = await this.minioClientService.findOne(p.id)
      pictureFile.url = p.url
      pictureFile.percentage = p.percentage
      newPics.push(pictureFile)
    }

    //修改音频文件
    let newRateFiles = []
    for await (const rateFile of songForm.rateFileInfos) {
      const fileInfo = await this.minioClientService.findOne(rateFile.fileId)
      const newRateFile = plainToClass(RateFileInfo, rateFile)
      fileInfo.format = rateFile.format || FileFormatEnum.UNKOWN
      fileInfo.mimeType = rateFile.mimeType
      fileInfo.percentage = rateFile.percentage
      fileInfo.transformProgress = rateFile.transformProgress
      fileInfo.status = rateFile.status
      newRateFile.fileInfo = fileInfo
      newRateFiles.push(newRateFile)
    }

    //分类
    data.genre = [cate]
    //封面
    data.pic = newPics
    //视频
    if (data.video) {
      data.video.title = data.title
      data.video.introduce = data.introduce
      data.video.artistList = artists
      data.video.genre = [cate]
      data.video.lang = data.lang
    }

    // data.video.poster = data.pic
    //歌手
    data.artistList = artists
    //音频
    data.rateFileInfos = newRateFiles
    return this.songRepository.save(data);
  }

  /**
   * 歌曲-表单数据
   * @param req 
   * @param id 
   * @returns 
   */
  getSongFormData(req: any, id: number) {
    return this.findOne(req, id);
  }

  /**
   * 歌曲-获取分类
   * @param ids 歌曲id列表 
   */
  getGenresByids(ids: number[]) {
    return this.dsong.getGenresByids(ids);
  }

  /**
    * 歌曲-新增-我收藏的歌曲
    * @param songForm 
    * @returns 
    */
  createfavorite(user: User, songForm: FavoriteForm) {
    this.dsong.createFavorite(user.id, songForm)
  }

  /**
  * 歌曲-删除-我收藏的歌曲
  * @param songForm 
  * @returns 
  */
  deletefavorite(user: User, songForm: FavoriteForm) {
    this.dsong.deleteFavorite(user.id, songForm)
  }

  /**
   * 歌曲-单个-新增
   * @param songForm 
   * @returns 
   */
  async saveSong(songForm: SongForm) {

    let song = plainToClass(Song, songForm)
    // song.pic = songForm.pic.at(0).url

    // let rateFileInfos = plainToClass(RateFileInfo, songForm.rateFileInfos)
    let artists = songForm.artistIds.map(async id => {
      return (await this.artistService.findOne(id))
    })


    song.artistList = (await Promise.all(artists))
    song.artistName = song.artistList.map(a => a.name)

    song.albumTitle = (await this.albumService.getOne(songForm.albumIds.at(0))).title
    const cate = new Category(+songForm.genre)
    song.video = null;
    let { rateFileInfos } = songForm
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect()

    await queryRunner.startTransaction()
    try {

      //插入song表
      const { generatedMaps, identifiers } = await queryRunner.manager
        .createQueryBuilder()
        .insert()
        .into(Song)
        .values(song)
        .execute()
      const songId = identifiers.at(0).id


      if (!songForm.video.id && songForm.video.objectName) {
        //插入视频表
        let video = plainToClass(Video, songForm.video)
        video.id = undefined;
        video.title = songForm.title;
        video.introduce = songForm.introduce
        video.artistList = song.artistList
        video.genre = [cate]
        video.lang = song.lang
        const { generatedMaps, identifiers } = await queryRunner.manager
          .createQueryBuilder()
          .insert()
          .into(Video)
          .values(video)
          .execute()
        const videoId = identifiers.at(0).id
        await queryRunner.manager
          .createQueryBuilder()
          .relation(Song, 'video')
          .of(songId)
          .set(videoId)
      }




      const pic = songForm.pic.at(0)
      if (pic) {
        const { id, ...rest } = pic
        //修改封面文件表
        await queryRunner.manager
          .createQueryBuilder()
          .update(FileInfo)
          .set({ url: rest.url, status: rest.status })
          .where("id = :fileId", { fileId: id })
          .execute()

        //设置封面文件和歌曲表的关联
        await queryRunner.manager
          .createQueryBuilder()
          .relation(FileInfo, 'song')
          .of(id)
          .set(songId)
      }


      //插入artist_song表的数据
      await queryRunner.manager
        .createQueryBuilder()
        .relation(Song, 'artistList')
        .of(songId)
        // .insert(songForm.artistIds)
        .add(songForm.artistIds)//多对多关系 新增与歌手的关系


      //插入song_cate表的数据
      await queryRunner.manager
        .createQueryBuilder()
        .relation(Song, 'genre')
        .of(songId)
        .add(songForm.genre)//多对多关系 新增与歌手的关系


      //修改音频文件
      for await (const item of rateFileInfos) {
        const { fileId, percentage, transformProgress, status, mimeType, format } = item;
        await queryRunner.manager
          .createQueryBuilder()
          .update(FileInfo)
          .set({ percentage, transformProgress, status, mimeType, format })
          .where("id = :fileId", { fileId })
          .execute()

      }

      //插入rate_file_info表的数据
      const rateFileRes = await queryRunner.manager
        .createQueryBuilder()
        .insert()
        .into(RateFileInfo)
        .values(rateFileInfos)
        .execute()



      //@OneToOne关系 新增ratefileinfo的fileinfo数据
      let rateIds = rateFileRes.identifiers.map(raw => raw.id)
      for await (const id of rateIds) {
        await queryRunner.manager
          .createQueryBuilder()
          .relation(RateFileInfo, 'fileInfo')
          .of(id)
          .set(songForm.rateFileInfos.at(rateIds.indexOf(id)).fileId)
      }


      //新增song表的ratefile数据
      await queryRunner.manager
        .createQueryBuilder()
        .relation(Song, 'rateFileInfos')
        .of(songId)
        .add(rateFileRes.identifiers.map(row => row.id))



      await queryRunner.commitTransaction()

    } catch (error) {
      Logger.error(error)
      await queryRunner.rollbackTransaction()
      throw new CommonException(EXCEPTION.COMMON)
    } finally {
      await queryRunner.release()
    }

    let date = plainToClass(Song, songForm)
    // return this.songRepository.save()



    return date
  }


  /**
   * 歌曲-分页-查询
   * @param param0 
   * @returns 
   */
  async findAll(user: User, pageQuery: SongPageQuery) {

    const [data, total] = await this.dsong.page(user?.username, pageQuery)

    let songVos: SongDto[] = await this.getList(user, data);

    return PageResDto.result(songVos, total)

  }

  /**
    * 歌曲-我收藏的歌曲
    * @param pageDto 
    * @returns 
    */
  async myfavorite(user: User, page: SongPageQuery) {
    const [data, total] = await this.dsong.myfavorite(user?.id, page);
    let songVos: SongDto[] = await this.getList(user, data);
    return PageResDto.result(songVos, total)
  }

  /**
   * 歌曲-下拉选项
   * @param songName 歌曲名称 
   * @returns 
   */
  async listSongOptions(songName: string, artistIds?: number[]) {
    const songs = await this.dsong.listBynameOrId(songName, artistIds)

    return songs.map<IOption<string, number>>(({ id, title }) => {
      const option = new IOption<string, number>()
      option.label = title
      option.value = id
      return option
    })
  }


  /**
   * 歌曲-单条-查询
   * @param id 
   * @returns 
   */
  async findOne(user: User, id: number) {



    // const song = await this.songRepository.findOne({
    //   select:{
    //     title:true
    //   },
    //   relations: [
    //     'artistList',
    //     'genre',
    //     'rateFileInfos',
    //     'paidUsers',
    //     'pic',
    //     'rateFileInfos.fileInfo'
    //   ], where: {
    //     id
    //   }
    // })




    const song = await this.dsong.getOneByIdAndUsername(id, user?.username)

    const songVo = plainToClass(SongDto, song)
    const pic = new MusicUploadFile()
    // pic.url = song.pic
    songVo.pic = song.pic
    songVo.genre = song.genre.at(0).id
    //艺术家
    songVo.artistIds = song.artistList.map(artist => artist.id)
    songVo.albumIds = song.album ? [song.album.id] : []
    //视频
    if (song.video) {
      const video = new VideoForm()
      video.id = song.video.id
      video.poster = song.video.poster
      video.url = (await this.minioClientService.presignedGetObject('private', song.video.objectName)).url
      songVo.video = video
    }

    songVo.rateFileInfos = song.rateFileInfos.map(rateFile => ({
      ...rateFile,
      transformProgress: rateFile.fileInfo.transformProgress,
      percentage: rateFile.fileInfo.percentage,
      fileId: rateFile.fileInfo.id,
      format: rateFile.fileInfo.format,
      name: rateFile.fileInfo.name,
      status: rateFile.fileInfo.status
    }))
    return songVo;
  }


  /**
   * 歌曲-封面-删除
   * @param id 
   */
  async deleteCoverPic(id: number, picId: string) {

    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      await queryRunner.manager
        .createQueryBuilder()
        .relation(Song, "pic")
        .of(id)
        .remove(picId);

      await queryRunner.manager
        .createQueryBuilder()
        .delete()
        .from(FileInfo)
        .where("id = :id", { id: picId })
        .execute()
      await queryRunner.commitTransaction()
    } catch (error) {
      Logger.error(error)
      await queryRunner.rollbackTransaction()
    } finally {
      await queryRunner.release()
    }


  }

  /**
   * 歌曲-音频文件-删除
   * @param id 
   * @param id 
   */
  async deleteRateFiles(id: number, rateIDs: number[]) {

    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {

      //取消和song之间的关联
      await queryRunner.manager
        .createQueryBuilder()
        .relation(Song, "rateFileInfos")
        .of(id)
        .remove(rateIDs);

      //删除ratefile 级联删除fileinfo
      await queryRunner.manager
        .createQueryBuilder()
        .delete()
        .from(RateFileInfo)
        .whereInIds(rateIDs)
        .execute()


      await queryRunner.commitTransaction()
    } catch (error) {
      Logger.error(error)
      await queryRunner.rollbackTransaction()
    } finally {
      await queryRunner.release()
    }


  }

  /**
   * 歌曲-单个删除
   * @param id 
   */
  async remove(id: number) {
    await this.songRepository.delete(id)
  }
  /**
   * 歌曲-批量删除
   * @param ids 
   */
  async removeByIds(ids: number[]) {
    await this.songRepository.delete(ids)
  }

  /**
   * 歌曲-单个软删除
   * @param id 
   */
  async softRemove(id: number) {
    await this.songRepository.softDelete(id)
  }
  /**
   * 歌曲-批量软删除
   * @param ids 
   */
  async softRemoveByIds(ids: number[]) {
    await this.songRepository.softDelete(ids)
  }

  /**
   * 歌曲-单条数据
   * @param id 
   * @returns 
   */
  getOne(id: number) {
    return this.dsong.getOne(id)
  }



  async getList(user: User, songs: Song[]) {
    let songVos: SongDto[] = []

    for await (const song of songs) {
      let songVo: SongDto = plainToClass(SongDto, song);
      songVo.allRate = song.rateFileInfos.map(rateFile => rateFile.rate)
      if (user?.username) {
        songVo.isFavorite = song.favoriteUsers.length > 0;
        songVo.isPaid = song.paidUsers.length > 0
      }

      songVo.pic = plainToClass(MusicUploadFile, song.pic)
      // songVo.lang = LangEnum.CN
      if (song.isVip) {
        const preRateFile = song.rateFileInfos.filter(rateFile => rateFile.isFull).at(0)
        songVo.trailAudioInfo.duration = preRateFile.duration
        songVo.trailAudioInfo.rate = preRateFile.rate
        songVo.trailAudioInfo.startTime = preRateFile.startTime
        songVo.trailAudioInfo.expireTime = preRateFile.expiredTime + Date.now()
        songVo.trailAudioInfo.path = await this.minioClientService.presignedGetObject(preRateFile.fileInfo.bucket, preRateFile.fileInfo.objectName, preRateFile.expiredTime)

      }
      songVos.push(songVo)
    }

    return songVos
  }

}
